Kotlin

Type-Safe Builders

Swift

A type-safe builder example

                  import com.example.html.* // see declarations below
​
fun result() =
    html {
        head {
            title {+"XML encoding with Kotlin"}
        }
        body {
            h1 {+"XML encoding with Kotlin"}
            p  {+"this format can be used as an alternative markup to XML"}
​
            // an element with attributes and text content
            a(href = "http://kotlinlang.org") {+"Kotlin"}
​
            // mixed content
            p {
                +"This is some"
                b {+"mixed"}
                +"text. For more see the"
                a(href = "http://kotlinlang.org") {+"Kotlin"}
                +"project"
            }
            p {+"some text"}
​
            // content generated by
            p {
                for (arg in args)
                    +arg
            }
        }
    }
                
                    import com.example.html.* // see declarations below
​
func result() =
    html {
        head {
            title {+"XML encoding with Kotlin"}
        }
        body {
            h1 {+"XML encoding with Kotlin"}
            p  {+"this format can be used as an alternative markup to XML"}
​
            // an element with attributes and text content
            a(href = "http://kotlinlang.org") {+"Kotlin"}
​
            // mixed content
            p {
                +"This is some"
                b {+"mixed"}
                +"text. For more see the"
                a(href = "http://kotlinlang.org") {+"Kotlin"}
                +"project"
            }
            p {+"some text"}
​
            // content generated by
            p {
                for (arg in args)
                    +arg
            }
        }
    }
                  

How it works

                  html {
 // ...
}
                
                    html {
 // ...
}
                  
                  fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()
    html.init()
    return html
}
                
                    func html(init: HTML.() -> Unit): HTML {
    let html = HTML()
    html.init()
    return html
}
                  
                  html {
    this.head { ... }
    this.body { ... }
}
                
                    html {
    this.head { ... }
    this.body { ... }
}
                  
                  html {
    head { ... }
    body { ... }
}
                
                    html {
    head { ... }
    body { ... }
}
                  
                  fun head(init: Head.() -> Unit) : Head {
    val head = Head()
    head.init()
    children.add(head)
    return head
}
​
fun body(init: Body.() -> Unit) : Body {
    val body = Body()
    body.init()
    children.add(body)
    return body
}
                
                    func head(init: Head.() -> Unit) : Head {
    let head = Head()
    head.init()
    children.add(head)
    return head
}
​
func body(init: Body.() -> Unit) : Body {
    let body = Body()
    body.init()
    children.add(body)
    return body
}
                  
                  protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
    tag.init()
    children.add(tag)
    return tag
}
                
                    protected func <T : Element> initTag(tag: T, init: T.() -> Unit): T {
    tag.init()
    children.add(tag)
    return tag
}
                  
                  fun head(init: Head.() -> Unit) = initTag(Head(), init)
​
fun body(init: Body.() -> Unit) = initTag(Body(), init)
                
                    func head(init: Head.() -> Unit) = initTag(Head(), init)
​
func body(init: Body.() -> Unit) = initTag(Body(), init)
                  
                  html {
    head {
        title {+"XML encoding with Kotlin"}
    }
    // ...
}
                
                    html {
    head {
        title {+"XML encoding with Kotlin"}
    }
    // ...
}
                  
                  operator fun String.unaryPlus() {
    children.add(TextElement(this))
}
                
                    operator func String.unaryPlus() {
    children.add(TextElement(this))
}
                  

Scope control: @DslMarker (since 1.1)

                  html {
    head {
        head {} // should be forbidden
    }
    // ...
}
                
                    html {
    head {
        head {} // should be forbidden
    }
    // ...
}
                  
                  @DslMarker
annotation class HtmlTagMarker
                
                    @DslMarker
annotation class HtmlTagMarker
                  
                  @HtmlTagMarker
abstract class Tag(val name: String) { ... }
                
                    @HtmlTagMarker
abstract class Tag(let name: String) { ... }
                  
                  class HTML() : Tag("html") { ... }
class Head() : Tag("head") { ... }
                
                    class HTML() : Tag("html") { ... }
class Head() : Tag("head") { ... }
                  
                  html {
    head {
        head { } // error: a member of outer receiver
    }
    // ...
}
                
                    html {
    head {
        head { } // error: a member of outer receiver
    }
    // ...
}
                  
                  html {
    head {
        this@html.head { } // possible
    }
    // ...
}
                
                    html {
    head {
        this@html.head { } // possible
    }
    // ...
}
                  

Full definition of the com.example.html package

                  package com.example.html
​
interface Element {
    fun render(builder: StringBuilder, indent: String)
}
​
class TextElement(val text: String) : Element {
    override fun render(builder: StringBuilder, indent: String) {
        builder.append("$indent$text\n")
    }
}
​
@DslMarker
annotation class HtmlTagMarker
​
@HtmlTagMarker
abstract class Tag(val name: String) : Element {
    val children = arrayListOf<Element>()
    val attributes = hashMapOf<String, String>()
​
    protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
        tag.init()
        children.add(tag)
        return tag
    }
​
    override fun render(builder: StringBuilder, indent: String) {
        builder.append("$indent<$name${renderAttributes()}>\n")
        for (c in children) {
            c.render(builder, indent + "  ")
        }
        builder.append("$indent</$name>\n")
    }
​
    private fun renderAttributes(): String {
        val builder = StringBuilder()
        for ((attr, value) in attributes) {
            builder.append(" $attr=\"$value\"")
        }
        return builder.toString()
    }
​
    override fun toString(): String {
        val builder = StringBuilder()
        render(builder, "")
        return builder.toString()
    }
}
​
abstract class TagWithText(name: String) : Tag(name) {
    operator fun String.unaryPlus() {
        children.add(TextElement(this))
    }
}
​
class HTML : TagWithText("html") {
    fun head(init: Head.() -> Unit) = initTag(Head(), init)
​
    fun body(init: Body.() -> Unit) = initTag(Body(), init)
}
​
class Head : TagWithText("head") {
    fun title(init: Title.() -> Unit) = initTag(Title(), init)
}
​
class Title : TagWithText("title")
​
abstract class BodyTag(name: String) : TagWithText(name) {
    fun b(init: B.() -> Unit) = initTag(B(), init)
    fun p(init: P.() -> Unit) = initTag(P(), init)
    fun h1(init: H1.() -> Unit) = initTag(H1(), init)
    fun a(href: String, init: A.() -> Unit) {
        val a = initTag(A(), init)
        a.href = href
    }
}
​
class Body : BodyTag("body")
class B : BodyTag("b")
class P : BodyTag("p")
class H1 : BodyTag("h1")
​
class A : BodyTag("a") {
    var href: String
        get() = attributes["href"]!!
        set(value) {
            attributes["href"] = value
        }
}
​
fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()
    html.init()
    return html
}
                
                    package com.example.html
​
interface Element {
    func render(builder: StringBuilder, indent: String)
}
​
class TextElement(let text: String) : Element {
    override func render(builder: StringBuilder, indent: String) {
        builder.append("$indent$text\n")
    }
}
​
@DslMarker
annotation class HtmlTagMarker
​
@HtmlTagMarker
abstract class Tag(let name: String) : Element {
    let children = arrayListOf<Element>()
    let attributes = hashMapOf<String, String>()
​
    protected func <T : Element> initTag(tag: T, init: T.() -> Unit): T {
        tag.init()
        children.add(tag)
        return tag
    }
​
    override func render(builder: StringBuilder, indent: String) {
        builder.append("$indent<$name${renderAttributes()}>\n")
        for (c in children) {
            c.render(builder, indent + "  ")
        }
        builder.append("$indent</$name>\n")
    }
​
    private func renderAttributes() -> String {
        let builder = StringBuilder()
        for ((attr, value) in attributes) {
            builder.append(" $attr=\"$value\"")
        }
        return builder.toString()
    }
​
    override func toString() -> String {
        let builder = StringBuilder()
        render(builder, "")
        return builder.toString()
    }
}
​
abstract class TagWithText(name: String) : Tag(name) {
    operator func String.unaryPlus() {
        children.add(TextElement(this))
    }
}
​
class HTML : TagWithText("html") {
    func head(init: Head.() -> Unit) = initTag(Head(), init)
​
    func body(init: Body.() -> Unit) = initTag(Body(), init)
}
​
class Head : TagWithText("head") {
    func title(init: Title.() -> Unit) = initTag(Title(), init)
}
​
class Title : TagWithText("title")
​
abstract class BodyTag(name: String) : TagWithText(name) {
    func b(init: B.() -> Unit) = initTag(B(), init)
    func p(init: P.() -> Unit) = initTag(P(), init)
    func h1(init: H1.() -> Unit) = initTag(H1(), init)
    func a(href: String, init: A.() -> Unit) {
        let a = initTag(A(), init)
        a.href = href
    }
}
​
class Body : BodyTag("body")
class B : BodyTag("b")
class P : BodyTag("p")
class H1 : BodyTag("h1")
​
class A : BodyTag("a") {
    var href: String
        get() = attributes["href"]!!
        set(value) {
            attributes["href"] = value
        }
}
​
func html(init: HTML.() -> Unit): HTML {
    let html = HTML()
    html.init()
    return html
}